﻿using System;
using System.Collections.Generic;
using FunctionalProgramming.PatternMatching.TransformingRules;

namespace FunctionalProgramming.PatternMatching
{
   /// <summary>
   /// Enables to add rules to transformation pattern matching job. Supports fluent interface.
   /// </summary>
   /// <typeparam name="TIn">The type of the input.</typeparam>
   /// <typeparam name="TOut">The type of the output.</typeparam>
   public class TransformationJob<TIn, TOut>
   {
      private readonly TIn originalInput;
      private readonly List<ITransformingRule<TIn, TOut>> transformationRules;

      /// <summary>
      /// Initializes a new instance of the <see cref="TransformationJob&lt;TIn, TOut&gt;"/> class.
      /// </summary>
      /// <param name="input">The input that will be transformed.</param>
      public TransformationJob(TIn input)
      {
         originalInput = input;
         transformationRules = new List<ITransformingRule<TIn, TOut>>();
      }

      /// <summary>
      /// If the value is equal to the provided input, it is replaced by output.
      /// </summary>
      /// <param name="input">The input.</param>
      /// <param name="output">The output.</param>
      /// <returns>Transfomration pattern matching job.</returns>
      public TransformationJob<TIn, TOut> Rule(TIn input, TOut output)
      {
         transformationRules.Add(new ConstantInputOutput<TIn, TOut>(input, output));
         return this;
      }

      /// <summary>
      /// If the predicate returns true, the input is replaced by output.
      /// </summary>
      /// <param name="predicate">The predicate.</param>
      /// <param name="output">The output.</param>
      /// <returns>Transfomration pattern matching job.</returns>
      public TransformationJob<TIn, TOut> Rule(Predicate<TIn> predicate, TOut output)
      {
         transformationRules.Add(new PredicateMatchingConstantOutput<TIn, TOut>(predicate, output));
         return this;
      }

      /// <summary>
      /// If the predicate returns true, output function is invoked and input is replaced by its result.
      /// </summary>
      /// <param name="predicate">The predicate.</param>
      /// <param name="output">The output.</param>
      /// <returns>Transfomration pattern matching job.</returns>
      public TransformationJob<TIn, TOut> Rule(Predicate<TIn> predicate, Func<TIn, TOut> output)
      {
         transformationRules.Add(new PredicateMatchingFuncOutput<TIn, TOut>(predicate, output));
         return this;
      }

      /// <summary>
      /// Any input is replaced by output.
      /// </summary>
      /// <param name="output">The output.</param>
      /// <returns>Transfomration pattern matching job.</returns>
      public TransformationJob<TIn, TOut> Default(TOut output)
      {
         transformationRules.Add(new Default<TIn, TOut>(output));
         return this;
      }

      /// <summary>
      /// Any input is replaced by result of output function invocation.
      /// </summary>
      /// <param name="output">The output.</param>
      /// <returns>Transfomration pattern matching job.</returns>
      public TransformationJob<TIn, TOut> Default(Func<TIn, TOut> output)
      {
         transformationRules.Add(new DefaultFunc<TIn, TOut>(output));
         return this;
      }

      /// <summary>
      /// Attempts to transform input by examining rules in order they were provided.
      /// </summary>
      /// <returns>Output from first rule that matched the input.</returns>
      /// <exception cref="T:System.InvalidOperationException">No rule that would match the input was found.</exception>
      public TOut DoTransformation()
      {
         TOut result = default(TOut);
         bool resultFound = false;

         foreach (var transformationRule in transformationRules)
         {
            if (transformationRule.DoesInputMatch(originalInput))
            {
               result = transformationRule.TransformInput(originalInput);
               resultFound = true;
               break;
            }
         }

         if (!resultFound)
         {
            throw new InvalidOperationException("No rule that would match the input was found.");
         }

         return result;
      }
   }
}